Custom Jupyter Widgets

The widget framework is built on top of the Comm framework (short for communication). The Comm framework is a framework that allows you send/receive JSON messages to/from the front end (as seen below).

To create a custom widget, you need to define the widget both in the browser and on the kernel size.

Python Kernel

DOMWidget and Widget

  • DOMWidget: Intended to be displayed in the Jupyter notebook
  • Widget: A terrible name for a synchronized object. It could not have any visual representation.

_view_name

Inheriting from the DOMWidget does not tell the widget framework what front end widget to associate with your back end widget. Instead, you must tell it yourself by defining a specially named traitlet, _view_name (as seen below).


In [1]:
import ipywidgets as widgets
from traitlets import Unicode


class HelloWidget(widgets.DOMWidget):

    _view_name = Unicode('HelloView').tag(sync=True)
    _view_module = Unicode('hello').tag(sync=True)

Front end (JavaScript)

Models and Views

Jupyter widgets rely on Backbone.js.

Backbone.js is an MVC (model view controller) framework.

Widgets defined in the back end are automatically synchronized with generic Backbone.js models in the front end. The traitlets are added to the front end instance automatically on first state push. The _view_name trait that you defined earlier is used by the widget framework to create the corresponding Backbone.js view and link that view to the model.

Import jupyter-js-widgets, define the view, implement the render method


In [2]:
%%javascript
require.undef('hello');

define('hello', ["@jupyter-widgets/base"], function(widgets) {
    
    var HelloView = widgets.DOMWidgetView.extend({
        
        // Render the view.
        render: function() { 
            this.$el.text('Hello World!'); 
        },
    });
    
    return {
        HelloView: HelloView
    };
});


Test

You should be able to display your widget just like any other widget now.


In [3]:
HelloWidget()


Making the widget stateful

Instead of displaying a static "hello world" message, we can display a string set by the back end.

  • First you need to add a traitlet in the back end.

    (Use the name of value to stay consistent with the rest of the widget framework and to allow your widget to be used with interact.)


In [4]:
class HelloWidget(widgets.DOMWidget):
    _view_name = Unicode('HelloView').tag(sync=True)
    _view_module = Unicode('hello').tag(sync=True)
    value = Unicode('Hello World!').tag(sync=True)

In [5]:
%%javascript
require.undef('hello');

define('hello', ["@jupyter-widgets/base"], function(widgets) {
    
    var HelloView = widgets.DOMWidgetView.extend({
        
        render: function() { 
            this.$el.text(this.model.get('value')); 
        },
    });
    
    return {
        HelloView : HelloView
    };
});


Dynamic updates

  • Adding and registering a change handler.

In [6]:
%%javascript
require.undef('hello');

define('hello', ["@jupyter-widgets/base"], function(widgets) {
    
    var HelloView = widgets.DOMWidgetView.extend({
        
        render: function() { 
            this.value_changed();
            this.model.on('change:value', this.value_changed, this);
        },
        
        value_changed: function() {
            this.$el.text(this.model.get('value')); 
        },
    });
    
    return {
        HelloView : HelloView
    };
});



In [7]:
w = HelloWidget()
w



In [8]:
w.value = 'test'

An example including bidirectional communication: A Spinner Widget


In [9]:
from traitlets import CInt


class SpinnerWidget(widgets.DOMWidget):
    _view_name = Unicode('SpinnerView').tag(sync=True)
    _view_module = Unicode('spinner').tag(sync=True)
    value = CInt().tag(sync=True)

In [10]:
%%javascript
requirejs.undef('spinner');

define('spinner', ["@jupyter-widgets/base"], function(widgets) {

    var SpinnerView = widgets.DOMWidgetView.extend({
        render: function() { 

            var that = this;
            this.$input = $('<input />');
            this.$el.append(this.$input);
            this.$spinner = this.$input.spinner({
                change: function( event, ui ) {
                    that.handle_spin(that.$spinner.spinner('value'));
                },
                spin: function( event, ui ) {
                    //ui.value is the new value of the spinner
                    that.handle_spin(ui.value);
                }
            });
            
            this.value_changed();
            this.model.on('change:value', this.value_changed, this);
        },
        
        value_changed: function() {
            this.$spinner.spinner('value', this.model.get('value'));
        },
        
        handle_spin: function(value) {
            this.model.set('value', value);
            this.touch();
        },
    });
    
    return {
        SpinnerView: SpinnerView
    };
});


Test of the spinner widget


In [11]:
w = SpinnerWidget(value=5)
w



In [12]:
w.value = 7

Wiring the spinner with another widget


In [13]:
from IPython.display import display
w1 = SpinnerWidget(value=0)
w2 = widgets.IntSlider()
display(w1,w2)

from traitlets import link
mylink = link((w1, 'value'), (w2, 'value'))



In [ ]:


In [ ]:


In [ ]:


In [ ]: